﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.IO;
using NPOI.HSSF.UserModel;
using NPOI.SS.UserModel;
using NPOI.SS.Util;

namespace ExcelReporting
{
    public class NPOIExcelDocumentConnector : IExcelDocument
    {
        private int SelectedSheetIndex { get; set; }
        
        public ISheet SelectedSheet 
        {
            get { return _workBook.GetSheetAt(SelectedSheetIndex); }
        }

        private readonly HSSFWorkbook _workBook;

        /// <summary>
        /// Созданные строки : Индекс страницы - список строк
        /// </summary>
        private readonly Dictionary<int, List<int>> _createdRows = new Dictionary<int, List<int>>();
        
        /// <summary>
        /// Путь для сохранения полученного документа
        /// </summary>
        private string _resultPath;

        /// <summary>
        /// Создает новый документ на основе шаблона(копия+дописывает)
        /// </summary>
        /// <param name="templatePath">Путь к шаблону</param>
        /// <param name="resultPath">Путь для сохранения полученного документа</param>
        public NPOIExcelDocumentConnector(string templatePath, string resultPath) 
        {
            _workBook = new HSSFWorkbook(new FileStream(templatePath, FileMode.Open));
            _resultPath = resultPath;
            SelectSheet(0);
        }

        /// <summary>
        /// Создает новый пустой документ
        /// </summary>
        /// <param name="resultPath">Путь для сохранения полученного документа</param>
        public NPOIExcelDocumentConnector(string resultPath)
        {
            _workBook = new HSSFWorkbook();
            _resultPath = resultPath;
            SelectSheet(0);
        }
        
        #region IExcelDocument implementation

        public void CopyDataTable(DataTable table, int startRowIndex, int startColumnIndex)
        {
            for (var i = 0; i < table.Rows.Count; i++)
            {
                var row = table.Rows[i];
                for (var j = 0; j < row.ItemArray.Length; j++)
                {
                    var val = row.ItemArray[j];
                    Int64 intVal;
                    if (Int64.TryParse(val.ToString(), out intVal))
                    {
                        SetNumericValue(startRowIndex + i, startColumnIndex + j, intVal, false);
                        continue;
                    }
                    this[startRowIndex + i, startColumnIndex + j] = row.ItemArray[j];
                }
            }
        }

        /// <summary>
        /// Получает и задает строковое значение ячейки
        /// </summary>
        public object this[int rowIndex, int columnIndex]
        {
            get
            {
                return GetCell(rowIndex, columnIndex).StringCellValue;
            }
            set
            {
                var cell = GetCell(rowIndex, columnIndex);
                cell.CellStyle.WrapText = true;
                cell.SetCellValue(value.ToString());
            }
        }

        /// <summary>
        /// Задает выбранному диапазону указанный стиль
        /// </summary>
        /// <param name="startRow">Индекс нач. строки</param>
        /// <param name="endRow">Индекс кон. строки</param>
        /// <param name="startColumn">Индекс нач. столбца</param>
        /// <param name="endColumn">Индекс кон. столбца</param>
        /// <param name="styleModes">Набор модификаторов нового стиля</param>
        /// <param name="recreateStyles">Флаг необходимости пересоздавать стиль. (Если false, то будет модифицирован старый стиль)</param>
        public void SetStyle(int startRow, int endRow, int startColumn, int endColumn, IEnumerable<StyleModes> styleModes, bool recreateStyles = true)
        {
            ICellStyle style;

            var cells = GetRange(startRow, endRow, startColumn, endColumn);
            
            foreach (var styleMode in styleModes)
            {
                //if (styleMode == StyleModes.IntegerDataFormat)
                //{
                //    foreach (var cell in cells)
                //    {
                //        cell.SetCellType(CellType.NUMERIC);
                //    }
                //}

                // Групповые(не для конкретных ячеек) стили
                if (styleMode == StyleModes.Merged)
                    _workBook.GetSheetAt(SelectedSheetIndex).AddMergedRegion(new CellRangeAddress(startRow, endRow, startColumn, endColumn));
            }

            if (recreateStyles)
            {
                style = _workBook.CreateCellStyle();
                ConfigureStyle(style, styleModes);
                foreach (var cell in cells)
                {
                    cell.CellStyle = style;
                }
            }
            else
            {
                foreach (var cell in cells)
                {
                    style = cell.CellStyle;
                    ConfigureStyle(style, styleModes);
                }
            }
        }

        /// <summary>
        /// Задает параметры стиля
        /// </summary>
        /// <param name="style">Ссылка на стиль</param>
        /// <param name="styleModes">Набор модификаторов стиля</param>
        private void ConfigureStyle(ICellStyle style, IEnumerable<StyleModes> styleModes)
        {
            // Default стили
            style.WrapText = true;

            foreach (var styleMode in styleModes)
            {
                switch (styleMode)
                {
                    case StyleModes.Bold:
                        IFont boldFont = _workBook.CreateFont();
                        boldFont.Boldweight = (short)FontBoldWeight.BOLD;
                        style.SetFont(boldFont);
                        break;
                    case StyleModes.RightHorizontalAlignment:
                        style.Alignment = HorizontalAlignment.RIGHT;
                        break;
                    case StyleModes.CenterHorizontalAlignment:
                        style.Alignment = HorizontalAlignment.CENTER;
                        break;
                    case StyleModes.SimpleBorders:
                        style.BorderRight = BorderStyle.THIN;
                        style.RightBorderColor = (IndexedColors.BLACK.Index);
                        style.BorderBottom = BorderStyle.THIN;
                        style.BottomBorderColor = (IndexedColors.BLACK.Index);
                        style.BorderLeft = BorderStyle.THIN;
                        style.LeftBorderColor = (IndexedColors.BLACK.Index);
                        style.BorderTop = BorderStyle.THIN;
                        style.TopBorderColor = (IndexedColors.BLACK.Index);
                        break;
                    case StyleModes.CurrencyDataFormat:
                        style.DataFormat = style.DataFormat = (_workBook.CreateDataFormat().GetFormat("0.00"));
                        break;
                    case StyleModes.TopVerticalAlignment:
                        style.VerticalAlignment = VerticalAlignment.TOP;
                        break;
                }
            }
        }

        /// <summary>
        /// Возвращает список ячеек из заданного диапазона
        /// </summary>
        /// <param name="row1">Начальная строка</param>
        /// <param name="row2">Конечная строка</param>
        /// <param name="col1">Начальный столбец</param>
        /// <param name="col2">Конечный столбец</param>
        /// <returns></returns>
        public IEnumerable<ICell> GetRange(int row1, int row2, int col1, int col2)
        {
            var cells = new List<ICell>();
            for (var i = row1; i < row2 + 1; i++)
            {
                for (var j = col1; j < col2 + 1; j++)
                {
                    cells.Add(GetCell(i, j));
                }
            }
            return cells;
        }

        /// <summary>
        /// Устанавливает денежное значение в ячейку
        /// </summary>
        /// <param name="row"></param>
        /// <param name="column"></param>
        /// <param name="value"></param>
        void IExcelDocument.SetCurrencyValue(int row, int column, double value)
        {
            SetNumericValue(row, column, value, true);
        }

        /// <summary>
        /// Устанавливает целочисленное значение в ячейку
        /// </summary>
        /// <param name="row"></param>
        /// <param name="column"></param>
        /// <param name="value"></param>
        void IExcelDocument.SetIntegerValue(int row, int column, int value)
        {
            SetNumericValue(row, column, value, false);
        }

        /// <summary>
        /// Устанавливает числовое значение в ячейку
        /// </summary>
        /// <param name="row"></param>
        /// <param name="column"></param>
        /// <param name="value"></param>
        /// <param name="withComma"></param>
        private void SetNumericValue(int row, int column, double value, bool withComma)
        {
            var cell = GetCell(row, column);
            cell.SetCellType(CellType.NUMERIC);
            cell.SetCellValue(value);
        }
        
        /// <summary>
        /// Завершает работу над документом 
        /// </summary>
        /// <param name="openFileOnCreate">Открывает файл если true</param>
        public void EndProcessing(bool openFileOnCreate)
        {
            Save();
            if (openFileOnCreate)
            {
                var process = new Process {StartInfo = {FileName = _resultPath}};
                process.Start();
            }
        }

        /// <summary>
        /// Удаляет все листы
        /// </summary>
        public void RemoveSheets()
        {
            while (_workBook.NumberOfSheets != 0)
            {
                _workBook.RemoveSheetAt(0);
            }
        }

        /// <summary>
        /// Задает высоту строки
        /// </summary>
        /// <param name="rowIndex">Индекс строки</param>
        /// <param name="height">Высота</param>
        public void SetRowHeight(int rowIndex, short height)
        {
            GetRow(rowIndex).Height = height;
        }

        /// <summary>
        /// Добавляет в документ новый лист
        /// </summary>
        /// <param name="name"></param>
        public void CreateSheet(string name)
        {
            _workBook.CreateSheet(name);
        }

        /// <summary>
        /// Выбирает лист по индексу
        /// </summary>
        /// <param name="sheetIndex"></param>
        public void SelectSheet(int sheetIndex)
        {
            SelectedSheetIndex = sheetIndex;
            if (_workBook.NumberOfSheets < sheetIndex + 1)
            {
                _workBook.CreateSheet();
            }
        }

        #endregion IExcelDocument implementation

        /// <summary>
        /// Возвращает строку по индексу
        /// </summary>
        /// <param name="rowIndex">Индекс строки</param>
        public IRow GetRow(int rowIndex)
        {
            var rowEnumerator = _workBook.GetSheetAt(SelectedSheetIndex).GetRowEnumerator();
            while (true)
            {
                var moved = rowEnumerator.MoveNext();
                if (moved)
                {
                    var row = (HSSFRow)rowEnumerator.Current;
                    if (row.RowNum == rowIndex)
                    {
                        rowEnumerator.Reset();
                        return row;
                    }
                }
                else 
                    break;
            }
            
            TryCreateRow(rowIndex);
            return _workBook.GetSheetAt(SelectedSheetIndex).GetRow(rowIndex);
        }

        private void TryCreateRow(int rowIndex)
        {
            if(!_createdRows.ContainsKey(SelectedSheetIndex))
                _createdRows[SelectedSheetIndex] = new List<int>();

            // Создаем(ищем созданную) строку
            if (!_createdRows[SelectedSheetIndex].Contains(rowIndex))
            {
                _createdRows[SelectedSheetIndex].Add(rowIndex);
                _workBook.GetSheetAt(SelectedSheetIndex).CreateRow(rowIndex);
            }
        }

        /// <summary>
        /// Возвращает ячейку по индексу
        /// </summary>
        private ICell GetCell(int rowIndex, int columnIndex)
        {
            var row = GetRow(rowIndex);
            if (!row.Cells.Exists(cell => cell.ColumnIndex == columnIndex))
            {
                row.CreateCell(columnIndex);
            }
            return row.Cells.Find(cell => cell.ColumnIndex == columnIndex);
        }

        /// <summary>
        /// Сохраняет результат работы в файл
        /// </summary>
        void Save()
        {
            var success = false;
            while (!success)
            {
                try
                {
                    if (File.Exists(_resultPath))
                        File.Delete(_resultPath);
                    success = true;
                }
                catch (IOException)
                {
                    // Если запускаем повторно, а старый файл открыт, то удалить не получится => дописываем Copy
                    _resultPath = _resultPath.Insert(_resultPath.Length - 1 - 3, "copy");
                }
            }
            var file = new FileStream(_resultPath, FileMode.CreateNew);
            _workBook.Write(file);
            file.Close();
        }
    }
}
